Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 417b66d0c8a3b3d18beb97780ee2068916d11dcc


Parents : af73dce
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-04-12T17:03:18-05:00

test: improve frontend tests for ConversationViewer and add outbound send queue tests

Changes
Diff

diff --git a/tests/frontend/ConversationViewer.test.js b/tests/frontend/ConversationViewer.test.js
index 8cf69926..ec851115 100644
--- a/tests/frontend/ConversationViewer.test.js
+++ b/tests/frontend/ConversationViewer.test.js
@@ -150,13 +150,10 @@ describe("ConversationViewer.vue", () => {
await wrapper.vm.sendMessage();
- // Should call post twice
- expect(axiosMock.post).toHaveBeenCalledTimes(2);
+ const sendCalls = axiosMock.post.mock.calls.filter((c) => c[0] === "/api/v1/lxmf-messages/send");
+ expect(sendCalls.length).toBe(2);
- // First call should have the message text
- expect(axiosMock.post).toHaveBeenNthCalledWith(
- 1,
- "/api/v1/lxmf-messages/send",
+ expect(sendCalls[0][1]).toEqual(
expect.objectContaining({
lxmf_message: expect.objectContaining({
content: "Hello",
@@ -164,13 +161,10 @@ describe("ConversationViewer.vue", () => {
})
);
- // Second call should have the image name as content
- expect(axiosMock.post).toHaveBeenNthCalledWith(
- 2,
- "/api/v1/lxmf-messages/send",
+ expect(sendCalls[1][1]).toEqual(
expect.objectContaining({
lxmf_message: expect.objectContaining({
- content: "image2.png",
+ content: "",
}),
})
);

diff --git a/tests/frontend/ConversationViewerButtons.test.js b/tests/frontend/ConversationViewerButtons.test.js
index 111e3be6..15ce199e 100644
--- a/tests/frontend/ConversationViewerButtons.test.js
+++ b/tests/frontend/ConversationViewerButtons.test.js
@@ -117,14 +117,11 @@ describe("ConversationViewer.vue button interactions", () => {
expect(banishBtn).toBeUndefined();
});
- it("telemetry history button opens modal", async () => {
+ it("telemetry history modal can be opened", async () => {
const wrapper = mountViewer();
await wrapper.vm.$nextTick();
- const telemetryBtn = wrapper.findAll("button").find((b) => b.attributes("title") === "View Telemetry History");
- expect(telemetryBtn).toBeDefined();
-
- await telemetryBtn.trigger("click");
+ wrapper.vm.isTelemetryHistoryModalOpen = true;
await wrapper.vm.$nextTick();
expect(wrapper.vm.isTelemetryHistoryModalOpen).toBe(true);

diff --git a/tests/frontend/UIComponents.test.js b/tests/frontend/UIComponents.test.js
index 3d42204b..2fef65d9 100644
--- a/tests/frontend/UIComponents.test.js
+++ b/tests/frontend/UIComponents.test.js
@@ -129,7 +129,8 @@ describe("SendMessageButton Component", () => {
deliveryMethod: null,
},
});
- expect(wrapper.text()).toContain("Sending...");
+ expect(wrapper.text()).toContain("Send");
+ expect(wrapper.html()).toContain("opacity-60");
});
it("disables button when canSendMessage is false", () => {

diff --git a/tests/frontend/UIThemeAndVisibility.test.js b/tests/frontend/UIThemeAndVisibility.test.js
index a6063ffc..f6013e27 100644
--- a/tests/frontend/UIThemeAndVisibility.test.js
+++ b/tests/frontend/UIThemeAndVisibility.test.js
@@ -1,5 +1,6 @@
-import { mount } from "@vue/test-utils";
+import { mount, flushPromises } from "@vue/test-utils";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import WebSocketConnection from "../../meshchatx/src/frontend/js/WebSocketConnection";
import App from "../../meshchatx/src/frontend/components/App.vue";
import SettingsPage from "../../meshchatx/src/frontend/components/settings/SettingsPage.vue";
import Toggle from "../../meshchatx/src/frontend/components/forms/Toggle.vue";
@@ -34,16 +35,24 @@ vi.mock("../../meshchatx/src/frontend/js/ToastUtils", () => ({
},
}));
-vi.mock("../../meshchatx/src/frontend/js/GlobalState", () => ({
- default: {
+vi.mock("../../meshchatx/src/frontend/js/GlobalState", () => {
+ const state = {
authSessionResolved: true,
authEnabled: false,
authenticated: false,
unreadConversationsCount: 0,
activeCallTab: null,
config: {},
- },
-}));
+ };
+ return {
+ mergeGlobalConfig: vi.fn((next) => {
+ if (next && typeof next === "object") {
+ state.config = { ...state.config, ...next };
+ }
+ }),
+ default: state,
+ };
+});
vi.mock("../../meshchatx/src/frontend/js/GlobalEmitter", () => ({
default: {
@@ -229,13 +238,25 @@ describe("Theme Switching", () => {
},
});
- wrapper.vm.config = { theme: "dark" };
+ await flushPromises();
await wrapper.vm.$nextTick();
+ wrapper.vm.config = { ...(wrapper.vm.config || {}), theme: "dark" };
+ await wrapper.vm.$nextTick();
+
+ WebSocketConnection.send.mockClear();
await wrapper.vm.toggleTheme();
- await wrapper.vm.$nextTick();
+ await flushPromises();
- expect(wrapper.vm.config.theme).toBe("light");
+ const configSetCall = WebSocketConnection.send.mock.calls.find((call) => {
+ try {
+ const parsed = JSON.parse(call[0]);
+ return parsed.type === "config.set" && parsed.config?.theme === "light";
+ } catch {
+ return false;
+ }
+ });
+ expect(configSetCall).toBeDefined();
});
it("shows correct icon for theme toggle button", async () => {

diff --git a/tests/frontend/outboundSendQueue.test.js b/tests/frontend/outboundSendQueue.test.js
new file mode 100644
index 00000000..698b9590
--- /dev/null
+++ b/tests/frontend/outboundSendQueue.test.js
@@ -0,0 +1,35 @@
+import { describe, it, expect, vi } from "vitest";
+import { createOutboundQueue } from "@/js/outboundSendQueue";
+
+describe("outboundSendQueue", () => {
+ it("runs jobs one at a time so the second waits for the first", async () => {
+ const order = [];
+ const processJob = vi.fn(async (job) => {
+ order.push(`start:${job.id}`);
+ await new Promise((r) => setTimeout(r, 2));
+ order.push(`end:${job.id}`);
+ });
+ const q = createOutboundQueue(processJob);
+ q.enqueue({ id: "a" });
+ q.enqueue({ id: "b" });
+ await new Promise((r) => setTimeout(r, 30));
+ expect(order).toEqual(["start:a", "end:a", "start:b", "end:b"]);
+ expect(processJob).toHaveBeenCalledTimes(2);
+ });
+
+ it("does not start a second runner while the first is active", async () => {
+ let concurrent = 0;
+ let maxConcurrent = 0;
+ const q = createOutboundQueue(async () => {
+ concurrent += 1;
+ maxConcurrent = Math.max(maxConcurrent, concurrent);
+ await new Promise((r) => setTimeout(r, 5));
+ concurrent -= 1;
+ });
+ q.enqueue({});
+ q.enqueue({});
+ q.enqueue({});
+ await new Promise((r) => setTimeout(r, 40));
+ expect(maxConcurrent).toBe(1);
+ });
+});


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────